"""
@author chenshi
@date 2023/3/14
定时任务
封装定时任务装饰器@Scheduler
封装支持持久化的定时任务添加方法Scheduler_add
定时任务持久化默认存储在data.db的apscheduler_jobs表中

对外开放管理类SchedulerManager
孪生数据存放在data.db的jobinfo表中
通过在网页修改该表的内容，实现对定时任务的管理。
"""

from plugin.db import Sqlite
from apscheduler.schedulers.background import BackgroundScheduler
from plugin.com_method import *
from plugin.mytime import MyTime
from plugin.logger import MyLogger
import traceback
from functools import wraps
import json

log = MyLogger.logger


class Trigger():

    def __init__(self, type, **kwargs) -> None:
        if type == 'cron':
            self.trigger = self.cron(**kwargs)
        elif type == 'interval':
            self.trigger = self.interval(**kwargs)
        elif type == 'date':
            self.trigger = self.date(**kwargs)

    class cron():
        """ cron定时器触发器类 """

        def __init__(self, ss='*', mm='*', hh='*', dd='*', MM='*', ww='*', yy='*', day_of_week='0-6', cron=None) -> None:
            """
            默认每秒执行一次 运行逻辑如下:
            :param ss: 秒
            :param mm: 分
            :param hh: 小时
            :param dd: 天
            :param MM: 月
            :param ww: 周
            :param yy: 年
            :param day_of_week: 星期几
            :param cron: cron表达式 优先级高于上面的参数 eg: '0 0 0 * * * *'
            :value eg:
                - * : 表示每次都执行
                - n : 表示值等于n时执行
                - n,m : 表示值等于n或者m时执行
                - n-m : 表示值在n到m之间每次都会执行
                - n/m : 表示以m分割，0-m之间，值等于n时执行
            """
            if not cron:
                self.second = ss
                self.minute = mm
                self.hour = hh
                self.day = dd
                self.month = MM
                self.week = ww
                self.year = yy
                self.day_of_week = day_of_week
            else:
                croninfo = cron.split(' ')
                if len(croninfo) != 7:
                    raise Exception('cron表达式错误')
                else:
                    self.second = croninfo[0]
                    self.minute = croninfo[1]
                    self.hour = croninfo[2]
                    self.day = croninfo[3]
                    self.month = croninfo[4]
                    self.week = croninfo[5]
                    self.year = croninfo[6]

    class interval():
        """ interval定时器触发器类 """

        def __init__(self, seconds=0, minutes=0, hours=0, days=0, weeks=0, start_date=None, end_date=None) -> None:
            """
            间隔指定时间执行
            :param seconds: 秒
            :param minutes: 分
            :param hours: 小时
            :param days: 天
            :param weeks: 周
            :param months: 月
            """
            self.seconds = seconds
            self.minutes = minutes
            self.hours = hours
            self.days = days
            self.weeks = weeks
            self.start_date = start_date
            self.end_date = end_date

    class date():
        """ date定时器触发器类 """

        def __init__(self, date_time) -> None:
            """
            指定时间执行
            :param date_time: 指定时间
            """
            self.date_time = date_time

    def cron_trigger_args_dict(cron_exp):
        if not cron_exp:
            return None
        # 将格式化的cron参数转换为字典
        cron_dict = {}
        # cron_exp 去掉尾部空格
        cron_exp = cron_exp.strip()
        cron_param_list = cron_exp.split(' ')
        if len(cron_param_list) not in (6, 7):
            raise Exception('cron参数错误')
        else:
            cron_dict['second'] = cron_param_list[0]
            cron_dict['minute'] = cron_param_list[1]
            cron_dict['hour'] = cron_param_list[2]
            cron_dict['day'] = cron_param_list[3]
            cron_dict['month'] = cron_param_list[4]
            cron_dict['week'] = cron_param_list[5]
            cron_dict['year'] = cron_param_list[6] if len(cron_param_list) == 7 else '*'
        return cron_dict


class JOBINFO():
    """ 定时任务的附加信息 """

    def __init__(self,
                 name,
                 description,
                 task,
                 args,
                 kwargs,
                 trigger=None,
                 trigger_type=None,
                 trigger_args=None,
                 is_active=1,
                 create_time=None,
                 update_time=None,
                 stack_info=None,
                 **ignore) -> None:
        """
        @param:
            name: 任务名称,唯一，与Jobs里面的id关联
            description: 任务描述
            task: 任务执行的方法
            args: 任务执行的参数
            kwargs: 任务执行的参数
            trigger: 任务触发器对象，如果传了trigger，则trigger_type和trigger_args无效
            trigger_type: 触发器类型
            trigger_args: 触发器参数
            is_active: 任务是否激活
            create_time: 任务创建时间
            stack_info: 任务创建时的堆栈信息
        """
        self.name = name
        self.description = description
        self.task = task
        self.args = tuple(args.split('&') if args else [])
        self.kwargs = kwargs
        if trigger:
            self.trigger_type = trigger.__class__.__name__
            self.trigger_args = trigger.__dict__
        else:
            # self.trigger = getattr(Trigger, trigger_type)(trigger_args)
            self.trigger_type = trigger_type
            if trigger_type == 'cron':
                self.trigger_args = Trigger.cron_trigger_args_dict(trigger_args)
            else:
                self.trigger_args = json.loads(trigger_args.replace("'", '"').replace('None', 'null'))
        self.is_active = is_active
        self.create_time = create_time or MyTime.timestr()
        self.update_time = update_time or MyTime.timestr()
        self.stack_info = stack_info or ''


# 定时任务执行的方法都通过JOB_CONTENT装饰器加入到这里
__job_content = []


# JOB_CONTENT装饰器,将方法放入__job_content
def JOB_CONTENT():

    def decorator(func):
        if func not in __job_content:
            __job_content.append(func)

        @wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)

        return wrapper

    return decorator


def get_job_by_name(func_name):
    for func in __job_content:
        if func.__name__ == func_name:
            return func
    return None


class SchedulerManager():
    """ 作业任务管理，管理所有的定时任务 """

    JOB_DEFAULTS_CONFIG = {
        'coalesce': True,  # 过时任务是否忽略
        'replace_existing': True,  # 如果存在相同id任务，是否替换
        'max_instances': 10,  # 最大实例数
        # 'misfire_grace_time': 60  # 容错时间
    }
    # 独立定时任务管理
    __detached_scheduler_dict = {}

    # region 独立定时任务管理
    @staticmethod
    def Scheduler(trigger, job_id=None, max_instances=1, delay=None):
        """
        独立定时任务装饰器。
        @param:
            trigger: 触发器对象，需要是Trigger的子类（cron, interval, date）
            max_instances: 最大实例数，默认为1，如果任务线程阻塞，将在下次执行。大于1时，将会安排其他线程执行
            delay: 延迟运行，单位秒 none:按照trigger执行，0:立即执行 >0:延迟执行
        @return:
            job_id: 作业id
        """

        def deractor(func):

            @wraps(func)
            def add_into_scheduler(*args, **kwargs):
                """
                @param:
                    func: 被装饰的函数
                    args: 函数参数
                    kwargs: 函数参数
                """
                return SchedulerManager.add_job(func, args, kwargs, trigger, job_id, max_instances, delay)

            return add_into_scheduler

        return deractor

    @staticmethod
    def add_job(func, args=None, kwargs=None, trigger=None, job_id=None, max_instances=1, delay=None):
        """
        添加一个独立的定时任务。
        @param:
            func: 定时任务函数
            trigger: 触发器对象，需要是Trigger的子类（cron, interval, date）
            job_id: 定时任务id，相同id的会被覆盖，如果不指定，则使用函数名+参数
            max_instances: 最大实例数，默认为1，如果任务线程阻塞，将在下次执行。大于1时，将会安排其他线程执行
            args: 函数参数
            kwargs: 函数参数
        @return:
            job_id: 定时任务id
        """
        try:
            assert trigger, '触发器不能为空'

            _detached_job = BackgroundScheduler(timezone='Asia/Shanghai')

            SchedulerManager.JOB_DEFAULTS_CONFIG['max_instances'] = max_instances
            job_id = job_id or func.__name__ + str(args) + str(kwargs)
            if delay is None:
                _detached_job.add_job(func,
                                      trigger.__class__.__name__,
                                      **trigger.__dict__,
                                      **SchedulerManager.JOB_DEFAULTS_CONFIG,
                                      id=job_id,
                                      args=args,
                                      kwargs=kwargs)
            elif delay >= 0:
                _detached_job.add_job(func,
                                      trigger.__class__.__name__,
                                      **trigger.__dict__,
                                      **SchedulerManager.JOB_DEFAULTS_CONFIG,
                                      id=job_id,
                                      next_run_time=MyTime.datetime_add(MyTime.now(), seconds=delay),
                                      args=args,
                                      kwargs=kwargs)
            
            _detached_job.start()
            SchedulerManager.__detached_scheduler_dict[job_id] = _detached_job
            log.info('++++++++++++++++++++++++++++++')
            log.info('定时任务 <{}> 已装载, delay = {}'.format(job_id, delay))
            return job_id
        except Exception as e:
            log.error(e)
            log.error(traceback.format_exc())
            return None

    @staticmethod
    def pause_job(job_id):
        """ 停止指定独立任务 """
        try:
            if job_id in list(SchedulerManager.__detached_scheduler_dict.keys()):
                SchedulerManager.__detached_scheduler_dict[job_id].pause_job(job_id)
                log.info('任务 <{}> 已停止'.format(job_id))
        except Exception as e:
            log.error(e)
            # print(traceback.format_exc())

    @staticmethod
    def resume_job(job_id):
        """ 恢复指定独立任务 """
        try:
            if job_id in SchedulerManager.__detached_scheduler_dict:
                SchedulerManager.__detached_scheduler_dict[job_id].resume_job(job_id)
                log.info('任务 <{}> 已恢复'.format(job_id))
        except Exception as e:
            log.error(e)
            # print(traceback.format_exc())

    @staticmethod
    def remove_job(job_id):
        """ 删除指定独立任务 """
        try:
            if job_id in SchedulerManager.__detached_scheduler_dict:
                SchedulerManager.__detached_scheduler_dict[job_id].remove_job(job_id)
                del SchedulerManager.__detached_scheduler_dict[job_id]
                log.info('任务 <{}> 已删除'.format(job_id))
        except Exception as e:
            log.error(e)
            # print(traceback.format_exc())

    # endregion


class Persistence_Scheduler_Manager():
    """ 持久化定时任务管理 """
    from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
    from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor

    # 存储配置
    SCHEDULER_JOBSTORES = {
        'default': SQLAlchemyJobStore(url='sqlite:///data.db')
        # 'type': 'sqlalchemy',
        # 'url': 'sqlite:///data.db',
        # 'tablename': 'jobs'
    }
    # 线程池配置
    SCHEDULER_EXECUTOR = {'default': ThreadPoolExecutor(10), 'processpool': ProcessPoolExecutor(10)}
    # 作业默认配置
    JOB_DEFAULTS_CONFIG = {
        'coalesce': True,  # 过时任务是否忽略
        'replace_existing': True,  # 如果存在相同id任务，是否替换
        'max_instances': 10,  # 最大实例数
        # 'misfire_grace_time': 60  # 容错时间
    }

    __jobs = None  # 存储所有定时任务
    __jobs_info = None  # 存储所有定时任务信息

    # 持久化定时任务关键属性,这些属性被修改，需要更新定时任务
    __presistence_job_important_attr = ['task', 'args', 'kwargs', 'trigger_type', 'trigger_args', 'is_active']

    def __init__(self):
        """ 使用类变量管理定时任务和任务信息，防止重复实例化带来的问题 """
        from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_EXECUTED
        if self.__class__.__jobs is None:
            self.__class__.__jobs = BackgroundScheduler(timezone='Asia/Shanghai',
                                                        jobstores=self.__class__.SCHEDULER_JOBSTORES,
                                                        executors=self.__class__.SCHEDULER_EXECUTOR)
            self.__class__.__jobs.start()
            self.__class__.__jobs.add_listener(self.__job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
            # 初始化任务信息
            Sqlite().execute('\
                create table if not exists jobinfo(\
                    name varcher(64) primary key not null,\
                    description varcher(128),\
                    task varchar(64),\
                    args varchar(256),\
                    kwargs varchar(256),\
                    trigger_type varchar(16),\
                    trigger_args varchar(128),\
                    trigger_args_simple varchar(128),\
                    is_active int,\
                    create_time datetime,\
                    update_time datetime,\
                    stack_info varchar(1024)\
                )\
                ')
            self.__class__.__jobs_info = [JOBINFO(**job_info_dict) for job_info_dict in Sqlite().select_to_dict('select * from jobinfo')]
            self.init_presistence_job()

    def __job_listener(self, event):
        """
        任务监听器。
        @param:
            event: 任务事件
        """
        if event.exception:
            log.error('任务出现异常：{}'.format(event.job_id))
            log.error(event.exception)
            # print(traceback.format_exc())
        else:
            # print('任务执行完毕：{}'.format(event.job_id))
            pass

    # region 持久化任务管理

    def init_presistence_job(self):
        """ 初始化持久化任务 """
        for jobinfo in self.__class__.__jobs_info:
            if not self.__class__.__jobs.get_job(jobinfo.name):
                log.info('初始化任务 <{}> 启动'.format(jobinfo.name))
                # print(jobinfo.args, jobinfo.kwargs)
                try:
                    self.__class__.__jobs.add_job(get_job_by_name(jobinfo.task),
                                                  jobinfo.trigger_type,
                                                  **jobinfo.trigger_args,
                                                  **self.JOB_DEFAULTS_CONFIG,
                                                  id=jobinfo.name,
                                                  args=jobinfo.args,
                                                  kwargs=convert_equlexp_to_dict(jobinfo.kwargs))
                except Exception as e:
                    log.error('初始化任务 <{}> 失败'.format(jobinfo.name))
                    log.error(e)
            else:
                if not jobinfo.is_active:
                    log.info('初始化任务 <{}> 停止'.format(jobinfo.name))
                    self.__class__.__jobs.pause_job(jobinfo.name)
                else:
                    log.info('初始化任务 <{}> 恢复'.format(jobinfo.name))

        for job in self.__class__.__jobs.get_jobs():
            if job.id not in [jobinfo.name for jobinfo in self.__class__.__jobs_info]:
                log.info('初始化任务 <{}> 删除'.format(job.id))
                self.__class__.__jobs.remove_job(job.id)

    def add_persistence_job(self, jobinfo: JOBINFO):
        """
        添加一个持久化任务。
        @param:
            jobinfo: 任务信息对象
        """
        try:
            if jobinfo.name in [_jobinfo.name for _jobinfo in self.__class__.__jobs_info]:
                self.update_persistence_job(jobinfo)
            else:
                self.__class__.__jobs_info.append(jobinfo)
                self.__class__.__jobs.add_job(get_job_by_name(jobinfo.task),
                                              jobinfo.trigger_type,
                                              **jobinfo.trigger_args,
                                              **self.JOB_DEFAULTS_CONFIG,
                                              args=jobinfo.args,
                                              kwargs=convert_equlexp_to_dict(jobinfo.kwargs),
                                              id=jobinfo.name)
                # print(jobinfo.trigger_args)
                log.info('添加持久化任务：{}'.format(jobinfo.name))
                # Sqlite().insert_object_list([jobinfo])
        except Exception as e:
            log.error(e)
            # print(traceback.format_exc())

    def update_persistence_job(self, jobinfo: JOBINFO):
        """
        更新一个持久化任务。
        @param:
            jobinfo: 任务信息对象
        """
        try:
            for jobinfo_index, _jobinfo in enumerate(self.__class__.__jobs_info):
                if _jobinfo.name == jobinfo.name and jobinfo.__dict__ != _jobinfo.__dict__:  # 如果任务名称相同，且任务信息不同，则更新
                    self.__class__.__jobs_info[jobinfo_index] = jobinfo
                    log.info('更新持久化任务：', jobinfo.name)
                    if jobinfo.is_active:
                        log.info('重启任务')
                        self.__class__.__jobs.resume_job(jobinfo.name)
                    else:
                        log.info('暂停任务')
                        self.__class__.__jobs.pause_job(jobinfo.name)
                    # 判断关键信息是否被修改
                    update_attr = [attr for attr in compare_object(jobinfo, _jobinfo) if attr in self.__class__.__presistence_job_important_attr]
                    if update_attr != []:  # 如果关键字段被修改，则更新任务

                        if jobinfo.is_active == 1:  # 如果触发器或者调用参数被修改，并且任务处于启动状态，则重新布置任务
                            self.__class__.__jobs.remove_job(jobinfo.name)
                            # log.info(jobinfo.args, jobinfo.kwargs)
                            self.__class__.__jobs.add_job(get_job_by_name(jobinfo.task),
                                                          jobinfo.trigger_type,
                                                          **jobinfo.trigger_args,
                                                          **self.JOB_DEFAULTS_CONFIG,
                                                          args=jobinfo.args,
                                                          kwargs=convert_equlexp_to_dict(jobinfo.kwargs),
                                                          id=jobinfo.name)
                    # Sqlite().update_object_list([jobinfo])
        except Exception as e:
            log.error(e)
            # print(traceback.format_exc())

    def remove_persistence_job(self, name):
        """
        删除一个持久化任务。
        @param:
            name: 任务名称
        """
        try:
            _jobinfo = self.get_persistence_jobinfo(name)
            if _jobinfo:
                self.__class__.__jobs_info.remove(_jobinfo)
                self.__class__.__jobs.remove_job(name)
                log.info('删除持久化任务：{}'.format(name))
        except Exception as e:
            log.error(e)
            # print(traceback.format_exc())

    def get_persistence_jobinfo(self, name):
        """
        获取指定名称的持久化任务。
        @param:
            name: 任务名称
        """
        jobs = [_jobinfo for _jobinfo in self.__class__.__jobs_info if _jobinfo.name == name]
        return None if len(jobs) == 0 else jobs[0]

    def refresh_presistence_job(self, name=None):
        if not name:
            # operate = {}
            jobs_new_info = [JOBINFO(**info) for info in Sqlite().select_to_dict('select * from jobinfo')]
            add, remove, upd = compare_object_list_by_attr(jobs_new_info, self.__class__.__jobs_info, 'name')
            for _jobinfo in add:
                self.add_persistence_job(_jobinfo)
            for _jobinfo in upd:
                self.update_persistence_job(_jobinfo)
            for _jobinfo in remove:
                self.remove_persistence_job(_jobinfo.name)

    # endregion


# if __name__ == '__main__':

#     @SchedulerManager.Scheduler(Trigger.interval(seconds=3))
#     def test_job(a, b):
#         print('{}:{}->{}'.format(a, b, datetime.now()))

#     # test_scheduler('arg1', 'arg2')
#     # test_scheduler('arg3', 'arg4')
#     # globals()['test_scheduler']()
#     # SchedulerManager.add_job(globals()['test_scheduler'], args=('arg0', 'arg1'), trigger=Trigger.interval(seconds=3), job_id='test_scheduler')
#     # scheduler = SchedulerManager()
#     # scheduler.add_persistence_job(
#     #     JOBINFO(name='test2',
#     #             description='这是任务1',
#     #             task='test_scheduler',
#     #             trigger=Trigger.interval(seconds=3),
#     #             is_active=1,
#     #             args=('arg0', 'arg1'),
#     #             kwargs={}))

#     # SchedulerManager()
#     id = test_job('arg0', 'arg1')
#     import time
#     while 1:
#         # 十秒后停止任务，二十秒后恢复任务，三十秒后删除任务
#         time.sleep(10)
#         SchedulerManager.pause_job(id)
#         time.sleep(10)
#         SchedulerManager.resume_job(id)
#         time.sleep(10)
#         SchedulerManager.remove_job(id)

#         # time.sleep(10)
#         # # scheduler.get_persistence_jobinfo('test2').is_active = 0
#         # # scheduler.refresh_presistence_job()
#         # SchedulerManager.pause_job('test1')
#         pass

if __name__ == '__main__':
    JOB_CONTENT.check_in_remind({'陈适1': '月朗心惜'})
